﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Net;
using System.Threading;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Resources;
using System.Xml.Linq;
using CacheLoader.Helper;

namespace CacheLoader
{
    /// <summary>
    /// The Loadr class responsible to all downloads and loading parts. 
    /// </summary>
    public class Loader : INotifyPropertyChanged
    {
        private readonly AssemblyPart _assemblyPart;
        private readonly object o = new object();
        private bool _needMoreSpace;
        private long _spaceNeeded;
        private bool inWriteProgress;
        private int total;

        /// <summary>
        /// Initializes a new instance of the <see cref="Loader"/> class.
        /// </summary>
        public Loader()
        {
            _assemblyPart = new AssemblyPart();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Loader"/> class.
        /// </summary>
        /// <param name="loadedComplete">delegate to be invokded when all parts loaded</param>
        public Loader(Action<object> loadedComplete)
            : this()
        {
            LoadComplete = loadedComplete;
        }

        /// <summary>
        /// Gets or sets the load complete delegate.
        /// This delegate (if not null) will called when all parts will be loaded. 
        /// </summary>
        /// <value>The load complete.</value>
        public Action<object> LoadComplete { get; set; }

        /// <summary>
        /// Gets or sets the manifest.
        /// </summary>
        /// <value>The manifest.</value>
        public CacheManifest RefreshManifest { get; set; }

        /// <summary>
        /// Gets or sets the local manifest.
        /// </summary>
        /// <value>The local manifest.</value>
        public CacheManifest LocalManifest { get; set; }

        /// <summary>
        /// Gets or sets the space needed.
        /// </summary>
        /// <value>The space needed.</value>
        public long SpaceNeeded
        {
            get { return _spaceNeeded; }
            set
            {
                _spaceNeeded = value + 1000; ;
                raisePropertyChanged("SpaceNeeded");
            }
        }

        public bool NeedMoreSpace
        {
            get { return _needMoreSpace; }
            private set
            {
                _needMoreSpace = value;
                raisePropertyChanged("NeedMoreSpace");
            }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        /// <summary>
        /// Loads the manifest. 
        /// </summary>
        /// <param name="manifestUri">The manifest URI.</param>
        /// <param name="loadComplete">The load complete.</param>
        public void LoadXamlManifest(Uri manifestUri)
        {
            var webClientc = new WebClient();
            webClientc.OpenReadCompleted += xamlManifest_OpenReadCompleted;
            webClientc.OpenReadAsync(manifestUri);
        }

        /// <summary>
        /// Loads the xaml manifest from stream.
        /// Can be used to load from local xap. 
        /// </summary>
        /// <param name="manifestStream">The manifest stream.</param>
        public void LoadXamlManifestFromStream(Stream manifestStream)
        {
            string manifestXaml = new StreamReader(manifestStream).ReadToEnd();
            RefreshManifest = XamlReader.Load(manifestXaml) as CacheManifest;
            if (RefreshManifest == null) throw new InvalidOperationException("manifest parse error");
            ProcessManifest();
        }

        private void xamlManifest_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            if (e.Error == null)
                LoadXamlManifestFromStream(e.Result);
            else
                throw e.Error;
        }


        /// <summary>
        /// Processes the manifest.
        /// Start downloading all parts. 
        /// </summary>
        private void ProcessManifest()
        {
            // todo: Load local manifest and compare items versions
            LocalManifest = ManifestUtil.LoadManifest(RefreshManifest.LocalPath);
            ManifestUtil.StoreManifest(RefreshManifest);
            if (LocalManifest != null) CompareManifests();

            RefreshManifest.Items.ForEach(cacheItem =>
                                              {
                                                  using (IsolatedStorageFile store =
                                                          IsolatedStorageFile.GetUserStoreForApplication())
                                                  {
                                                      if (!store.FileExists(cacheItem.LocalPath) || cacheItem.NeedRefresh)
                                                      {
                                                          var webClient = new WebClient();
                                                          cacheItem.Cached = false;
                                                          webClient.OpenReadCompleted += chachItem_OpenReadCompleted;
                                                          webClient.OpenReadAsync(
                                                              new Uri(cacheItem.Path, UriKind.Relative),
                                                              cacheItem.ItemName);
                                                          cacheItem.Status = PartStatus.Downloading;


                                                      }

                                                      else
                                                      {
                                                          cacheItem.Stored = true;
                                                          cacheItem.ItemStream = store.OpenFile(cacheItem.LocalPath,
                                                                                                FileMode.Open,
                                                                                                FileAccess.Read);
                                                          cacheItem.Cached = true;
                                                      }
                                                  }
                                              });
            if (RefreshManifest.Items.All(p => p.Cached)) // all parts already cached
            {
                LoadParts();
            }
        }

        private void CompareManifests()
        {
            RefreshManifest.Items.ForEach(item =>
                                              {
                                                  CacheItem localItem =
                                                      LocalManifest.Items.Where(p => p.ItemName == item.ItemName).
                                                          FirstOrDefault();
                                                  if (localItem == null)
                                                      localItem.NeedRefresh = true;
                                                  else
                                                  {
                                                      Version local, refreshed;
                                                      if (Version.TryParse(localItem.Version, out local))
                                                          if (Version.TryParse(item.Version, out refreshed))
                                                              item.NeedRefresh = local < refreshed;
                                                  }
                                              });
        }

        /// <summary>
        /// Loads the parts of the manifest from cache. 
        /// </summary>
        private void LoadParts()
        {
            RefreshManifest.Items.ForEach(cacheItem =>
                                              {
                                                  if (cacheItem.Path.EndsWith("dll"))
                                                      LoadAssembyCacheItem(cacheItem);
                                                  if (cacheItem.Path.EndsWith("xap"))
                                                      LoadXapCacheItem(cacheItem);
                                              });

            StoreItems();

            if (LoadComplete != null)
                LoadComplete(this);
        }

        /// <summary>
        /// Stores the items locacly on Isolated storage.
        /// If there isn'y enough space the NeedMoreSpace and SpaceNeeded changed.
        /// </summary>
        public void StoreItems()
        {
            using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                long totalSize = RefreshManifest.Items.Where(p => p.Cached && !p.Stored)
                    .Sum(p => p.ItemStream.Length);
                if (totalSize > store.AvailableFreeSpace)
                {
                    NeedMoreSpace = true;
                    SpaceNeeded = totalSize - store.AvailableFreeSpace;
                }
                else
                    RefreshManifest.Items.Where(p => p.Cached && !p.Stored).ToList()
                        .ForEach(WriteCacheItem);
            }
        }

        // base on Tim  heuer code - http://timheuer.com/blog/archive/2008/09/24/silverlight-isolated-storage-caching.aspx
        // Some changes made to parse the manifest with Linq2XML because of API changes in SL2. 
        private void LoadXapCacheItem(CacheItem cacheItem)
        {
            cacheItem.Status = PartStatus.LoadingToMemory;
            Stream xap = cacheItem.ItemStream;
            string xapManifest =
                new StreamReader(
                    Application.GetResourceStream(new StreamResourceInfo(xap, null),
                                                  new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd();
            XElement manifestRoot = XDocument.Parse(xapManifest).Root;
            List<XElement> parts = manifestRoot.Elements().Elements().ToList();

            foreach (XElement asmPart in parts)
            {
                string source = asmPart.Attribute("Source").Value;
                StreamResourceInfo streamInfo =
                    Application.GetResourceStream(new StreamResourceInfo(xap, "application/binary"),
                                                  new Uri(source, UriKind.Relative));

                _assemblyPart.Load(streamInfo.Stream);
            }
            cacheItem.Status = PartStatus.Complete;
        }

        /// <summary>
        /// Loads the assemby cached item.
        /// </summary>
        /// <param name="cacheItem">The cache item.</param>
        private void LoadAssembyCacheItem(CacheItem cacheItem)
        {
            Stream assembly = cacheItem.ItemStream;
            _assemblyPart.Load(assembly);
        }
#if DEBUG
        int delay = 1500;
#endif
        private void chachItem_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            if (e.Error != null)
                throw e.Error;
#if DEBUG

            Thread.Sleep(delay);
            delay += 1500;
#endif
            CacheItem item = RefreshManifest.Items.Where(p => p.ItemName == ((string)e.UserState)).FirstOrDefault();
            item.ItemStream = e.Result;
            item.Cached = true;
            if (RefreshManifest.Items.All(p => p.Cached))
                LoadParts();
        }

        /// <summary>
        /// Write stream to isolated storage file. 
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <param name="fileStream">The file stream.</param>
        private void WriteCacheItem(CacheItem item)
        {
            var buffer = new byte[4096];
            int bytesRead;

            using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
            {
                IsolatedStorageFileStream fileStream = store.OpenFile(item.LocalPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
                while ((bytesRead = item.ItemStream.Read(buffer, 0, buffer.Length)) != 0)
                {
                    fileStream.Write(buffer, 0, bytesRead);
                }
                fileStream.Close();
            }
        }

        private void raisePropertyChanged(string propName)
        {
            if (PropertyChanged != null)
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
        }
    }
}